/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.ranger.credentialapi;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.alias.CredentialShell;
import org.apache.hadoop.util.GenericOptionsParser;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.security.KeyStore;
import java.util.Arrays;

public class buildks {
    public static void main(String[] args) {
        buildks buildksOBJ = new buildks();
        String  command    = null;
        try {
            if (args != null && args.length >= 3) {
                command = args[0];
                if (command != null && !command.trim().isEmpty()) {
                    if (command.equalsIgnoreCase("create")) {
                        buildksOBJ.createCredential(args);
                    } else if (command.equalsIgnoreCase("list")) {
                        buildksOBJ.listCredential(args);
                    } else {
                        System.out.println(command + " is not supported in current version of CredentialBuilder API.");
                        System.exit(1);
                    }
                }
            } else {
                System.out.println("Invalid Command line argument.");
                System.exit(1);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            System.exit(1);
        }
    }

    public static boolean isValidCreateCommand(String command, String alias, String valueOption, String credential, String providerOption, String providerPath, String storeTypeOption, String storeType) {
        boolean isValid = true;
        try {
            if (command == null || !"create".equalsIgnoreCase(command.trim())) {
                System.out.println("Invalid create phrase in credential creation command!!");
                System.out.println("Expected:'create' Found:'" + command + "'");
                displaySyntax("create", storeType);
                return false;
            }
            if (alias == null || "".equalsIgnoreCase(alias.trim())) {
                System.out.println("Invalid alias name phrase in credential creation command!!");
                System.out.println("Found:'" + alias + "'");
                displaySyntax("create", storeType);
                return false;
            }
            if (valueOption == null || !"-value".equalsIgnoreCase(valueOption.trim())) {
                System.out.println("Invalid value option in credential creation command!!");
                System.out.println("Expected:'-value' Found:'" + valueOption + "'");
                displaySyntax("create", storeType);
                return false;
            }
            if (credential == null) {
                System.out.println("Invalid credential value in credential creation command!!");
                System.out.println("Found:" + credential);
                displaySyntax("create", storeType);
                return false;
            }
            if (providerOption == null || !"-provider".equalsIgnoreCase(providerOption.trim())) {
                System.out.println("Invalid provider option in credential creation command!!");
                System.out.println("Expected:'-provider' Found:'" + providerOption + "'");
                displaySyntax("create", storeType);
                return false;
            }
            if (providerPath == null || "".equalsIgnoreCase(providerPath.trim()) || ((!providerPath.startsWith("localjceks://") && !providerPath.startsWith("jceks://")) && (!providerPath.startsWith("localbcfks://") && !providerPath.startsWith("bcfks://")))) {
                System.out.println("Invalid provider option in credential creation command!!");
                System.out.println("Found:'" + providerPath + "'");
                displaySyntax("create", storeType);
                return false;
            }
        } catch (Exception ex) {
            System.out.println("Invalid input or runtime error! Please try again.");
            System.out.println("Input:" + command + " " + alias + " " + valueOption + " " + credential + " " + providerOption + " " + providerPath);
            displaySyntax("create", storeType);
            ex.printStackTrace();
            return false;
        }
        return isValid;
    }

    public static boolean isValidListCommand(String command, String providerOption, String providerPath, String storeTpe) {
        boolean isValid = true;
        try {
            if (command == null || !"list".equalsIgnoreCase(command.trim())) {
                System.out.println("Invalid list phrase in credential get command!!");
                System.out.println("Expected:'list' Found:'" + command + "'");
                displaySyntax("list", storeTpe);
                return false;
            }

            if (providerOption == null || !"-provider".equalsIgnoreCase(providerOption.trim())) {
                System.out.println("Invalid provider option in credential get command!!");
                System.out.println("Expected:'-provider' Found:'" + providerOption + "'");
                displaySyntax("list", storeTpe);
                return false;
            }
            if (providerPath == null || "".equalsIgnoreCase(providerPath.trim()) || ((!providerPath.startsWith("localjceks://") && !providerPath.startsWith("jceks://")) && (!providerPath.startsWith("localbcfks://") && !providerPath.startsWith("bcfks://")))) {
                System.out.println("Invalid provider option in credential get command!!");
                System.out.println("Found:'" + providerPath + "'");
                displaySyntax("list", storeTpe);
                return false;
            }
        } catch (Exception ex) {
            System.out.println("Invalid input or runtime error! Please try again.");
            System.out.println("Input:" + command + " " + providerOption + " " + providerPath);
            displaySyntax("list", storeTpe);
            ex.printStackTrace();
            return false;
        }
        return isValid;
    }

    public static void displayCommand(String[] args) {
        String debugOption = System.getProperty("debug");
        if (debugOption != null && "TRUE".equalsIgnoreCase(debugOption)) {
            StringBuilder tempBuffer = new StringBuilder("");
            if (args != null && args.length > 0) {
                for (String arg : args) {
                    tempBuffer.append(arg).append(" ");
                }
                System.out.println("Command to execute:[" + tempBuffer + "]");
            }
        }
    }

    public static void displaySyntax(String command, String storeType) {
        if ("bcfks".equalsIgnoreCase(storeType)) {
            if (command != null && command.trim().equalsIgnoreCase("create")) {
                System.out.println("Correct syntax is:create <aliasname> -value <password> -provider <bcfks://file/filepath>");
                System.out.println("sample command is:create myalias -value password123 -provider bcfks://file/tmp/ks/myks.bcfks");
            }
            if (command != null && command.trim().equalsIgnoreCase("list")) {
                System.out.println("Correct syntax is:list -provider <bcfks://file/filepath>");
                System.out.println("sample command is:list -provider bcfks://file/tmp/ks/myks.bcfks");
            }
            if (command != null && command.trim().equalsIgnoreCase("get")) {
                System.out.println("Correct syntax is:get <aliasname> -provider <bcfks://file/filepath>");
                System.out.println("sample command is:get myalias -provider bcfks://file/tmp/ks/myks.bcfks");
            }
        } else {
            if (command != null && command.trim().equalsIgnoreCase("create")) {
                System.out.println("Correct syntax is:create <aliasname> -value <password> -provider <jceks://file/filepath>");
                System.out.println("sample command is:create myalias -value password123 -provider jceks://file/tmp/ks/myks.jceks");
            }
            if (command != null && command.trim().equalsIgnoreCase("list")) {
                System.out.println("Correct syntax is:list -provider <jceks://file/filepath>");
                System.out.println("sample command is:list -provider jceks://file/tmp/ks/myks.jceks");
            }
            if (command != null && command.trim().equalsIgnoreCase("get")) {
                System.out.println("Correct syntax is:get <aliasname> -provider <jceks://file/filepath>");
                System.out.println("sample command is:get myalias -provider jceks://file/tmp/ks/myks.jceks");
            }
        }
    }

    public static boolean isValidGetCommand(String command, String alias, String providerOption, String providerPath, String storeType) {
        boolean isValid = true;
        try {
            if (command == null || !"get".equalsIgnoreCase(command.trim())) {
                System.out.println("Invalid get phrase in credential get command!!");
                System.out.println("Expected:'get' Found:'" + command + "'");
                displaySyntax("get", storeType);
                return false;
            }
            if (alias == null || "".equalsIgnoreCase(alias.trim())) {
                System.out.println("Invalid alias name phrase in credential get command!!");
                System.out.println("Found:'" + alias + "'");
                displaySyntax("get", storeType);
                return false;
            }
            if (providerOption == null || !"-provider".equalsIgnoreCase(providerOption.trim())) {
                System.out.println("Invalid provider option in credential get command!!");
                System.out.println("Expected:'-provider' Found:'" + providerOption + "'");
                displaySyntax("get", storeType);
                return false;
            }
            if (providerPath == null || "".equalsIgnoreCase(providerPath.trim()) || (!providerPath.startsWith("localjceks://") && !providerPath.startsWith("jceks://")) && (!providerPath.startsWith("localbcfks://") && !providerPath.startsWith("bcfks://"))) {
                System.out.println("Invalid provider option in credential get command!!");
                System.out.println("Found:'" + providerPath + "'");
                displaySyntax("get", storeType);
                return false;
            }
        } catch (Exception ex) {
            System.out.println("Invalid input or runtime error! Please try again.");
            System.out.println("Input:" + command + " " + alias + " " + providerOption + " " + providerPath);
            displaySyntax("get", storeType);
            ex.printStackTrace();
            return false;
        }
        return isValid;
    }

    public int createCredential(String[] args) {
        int    returnCode      = -1;
        String command         = null;
        String alias           = null;
        String valueOption     = null;
        String credential      = null;
        String providerOption  = null;
        String providerPath    = null;
        String tempCredential  = null;
        String storeTypeOption = "storeType";
        String storeType       = KeyStore.getDefaultType();
        try {
            if (args != null && (args.length == 6 || args.length == 8)) {
                command        = args[0];
                alias          = args[1];
                valueOption    = args[2];
                credential     = args[3];
                providerOption = args[4];
                providerPath   = args[5];
                if (args.length == 8) {
                    storeTypeOption = args[6];
                    storeType       = args[7];
                }
                if (!isValidCreateCommand(command, alias, valueOption, credential, providerOption, providerPath, storeTypeOption, storeType)) {
                    return returnCode;
                }
                deleteInvalidKeystore(providerPath);
                tempCredential = CredentialReader.getDecryptedString(providerPath, alias, storeType);
            } else {
                return returnCode;
            }

            if (tempCredential == null) {
                returnCode = createKeyStore(args);
            } else {
                try {
                    System.out.println("The alias " + alias + " already exists!! Will try to delete first.");
                    boolean isSilentMode = true;
                    String[]  argsDelete = new String[4];
                    argsDelete[0] = "delete";
                    argsDelete[1] = alias;
                    argsDelete[2] = providerOption;
                    argsDelete[3] = providerPath;
                    returnCode = deleteCredential(argsDelete, isSilentMode);
                    if (returnCode == 0) {
                        returnCode = createKeyStore(args);
                    }
                } catch (Exception ex) {
                    returnCode = -1;
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return returnCode;
    }

    public int createKeyStore(String[] args) {
        int returnCode = -1;
        try {
            String command         = null;
            String alias           = null;
            String valueOption     = null;
            String credential      = null;
            String providerOption  = null;
            String providerPath    = null;
            String storeTypeOption = "storeType";
            String storeType       = KeyStore.getDefaultType();
            if (args != null && (args.length == 6 || args.length == 8)) {
                command        = args[0];
                alias          = args[1];
                valueOption    = args[2];
                credential     = args[3];
                providerOption = args[4];
                providerPath   = args[5];
                if (args.length == 8) {
                    storeTypeOption = args[6];
                    storeType       = args[7];
                }
                if (!isValidCreateCommand(command, alias, valueOption, credential, providerOption, providerPath, storeTypeOption, storeType)) {
                    return returnCode;
                }
                displayCommand(args);
            } else {
                return returnCode;
            }

            CredentialShell cs   = new CredentialShell();
            Configuration   conf = new Configuration();
            //parse argument
            GenericOptionsParser parser = new GenericOptionsParser(conf, args);
            //set the configuration back, so that Tool can configure itself
            cs.setConf(conf);
            //get valid and remaining argument
            String[] toolArgs = parser.getRemainingArgs();
            //execute command in CredentialShell
            // int i = 0;
            //for(String s : toolArgs) {
            //System.out.println("TooArgs [" + i + "] = [" + s + "]");
            //i++;
            // }
            String[] finalArgs = Arrays.copyOfRange(toolArgs, 0, 6);
            returnCode = cs.run(finalArgs);
            //if response code is zero then success else failure
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return returnCode;
    }

    public int createCredentialFromUserInput() {
        int returnCode = -1;
        try {
            String[] args            = null;
            String   command         = null;
            String   alias           = null;
            String   valueOption     = null;
            String   credential      = null;
            String   providerOption  = null;
            String   providerPath    = null;
            String   storeTypeOption = null;
            String   storeType       = null;
            //below code can ask user to input if command line input fails
            System.out.println("Enter Alias Name:");
            BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
            alias = bufferRead.readLine();
            System.out.println("Enter password:");
            credential = bufferRead.readLine();
            System.out.println("Enter .jceks output file name with path:");
            providerPath = bufferRead.readLine();
            if (providerPath != null && !providerPath.trim().isEmpty() && (!providerPath.startsWith("localjceks://file") && !providerPath.startsWith("jceks://file") && !providerPath.startsWith("localbcfks://file") && !providerPath.startsWith("bcfks://file"))) {
                if (providerPath.startsWith("/")) {
                    providerPath = "jceks://file" + providerPath;
                } else {
                    providerPath = "jceks://file/" + providerPath;
                }
            }
            command        = "create";
            valueOption    = "-value";
            providerOption = "-provider";
            if (!isValidCreateCommand(command, alias, valueOption, credential, providerOption, providerPath, storeTypeOption, storeType)) {
                return returnCode;
            }
            args    = new String[6];
            args[0] = command;
            args[1] = alias;
            args[2] = valueOption;
            args[3] = credential;
            args[4] = providerOption;
            args[5] = providerPath;
            CredentialShell cs   = new CredentialShell();
            Configuration   conf = new Configuration();
            //parse argument
            GenericOptionsParser parser = new GenericOptionsParser(conf, args);
            //set the configuration back, so that Tool can configure itself
            cs.setConf(conf);
            //get valid and remaining argument
            String[] toolArgs = parser.getRemainingArgs();
            //execute command in CredentialShell
            returnCode = cs.run(toolArgs);
            //if response code is zero then success else failure
            //System.out.println("Response Code:"+returnCode);
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return returnCode;
    }

    public int listCredential(String[] args) {
        int    returnCode     = -1;
        String command        = null;
        String providerOption = null;
        String providerPath   = null;
        String storeType      = KeyStore.getDefaultType();
        try {
            if (args != null && args.length == 3) {
                command        = args[0];
                providerOption = args[1];
                providerPath   = args[2];
                if (!isValidListCommand(command, providerOption, providerPath, storeType)) {
                    return returnCode;
                }
                //display command which need to be executed or entered
                displayCommand(args);
            } else {
                return returnCode;
            }
            CredentialShell cs   = new CredentialShell();
            Configuration   conf = new Configuration();
            //parse argument
            GenericOptionsParser parser = new GenericOptionsParser(conf, args);
            //set the configuration back, so that Tool can configure itself
            cs.setConf(conf);
            //get valid and remaining argument
            String[] toolArgs = parser.getRemainingArgs();
            //execute command in CredentialShell
            returnCode = cs.run(toolArgs);
            //if response code is zero then success else failure
            //System.out.println("Response Code:"+returnCode);
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return returnCode;
    }

    public int deleteCredential(String[] args, boolean isSilentMode) {
        int returnCode = -1;
        try {
            if (args != null && args.length == 4) {
                // for non-interactive, insert argument "-f" if needed
                if (isSilentMode && isCredentialShellInteractiveEnabled()) {
                    String[] updatedArgs = new String[5];
                    updatedArgs[0] = args[0];
                    updatedArgs[1] = args[1];
                    updatedArgs[2] = "-f";
                    updatedArgs[3] = args[2];
                    updatedArgs[4] = args[3];

                    args = updatedArgs;
                }

                //display command which need to be executed or entered
                displayCommand(args);
            } else {
                return returnCode;
            }
            CredentialShell cs   = new CredentialShell();
            Configuration   conf = new Configuration();
            //parse argument
            GenericOptionsParser parser = new GenericOptionsParser(conf, args);
            //set the configuration back, so that Tool can configure itself
            cs.setConf(conf);
            //get valid and remaining argument
            String[] toolArgs = parser.getRemainingArgs();
            //execute command in CredentialShell
            // String[] finalArgs = Arrays.copyOfRange(toolArgs, 0, 6);
            returnCode = cs.run(toolArgs);
            //if response code is zero then success else failure
            //System.out.println("Response Code:"+returnCode);
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return returnCode;
    }

    public String getCredential(String[] args) {
        String command        = null;
        String alias          = null;
        String providerOption = null;
        String providerPath   = null;
        String tempCredential = null;
        String storeType      = KeyStore.getDefaultType();
        try {
            if (args != null && args.length == 4) {
                command        = args[0];
                alias          = args[1];
                providerOption = args[2];
                providerPath   = args[3];
                if (!isValidGetCommand(command, alias, providerOption, providerPath, storeType)) {
                    displaySyntax("get", storeType);
                } else {
                    tempCredential = CredentialReader.getDecryptedString(providerPath, alias, storeType);
                }
            } else {
                displaySyntax("get", storeType);
            }
            if (tempCredential == null) {
                System.out.println("Alias " + alias + " does not exist!!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return tempCredential;
    }

    public void deleteInvalidKeystore(String providerPath) {
        if (providerPath != null) {
            String keystore = null;
            if (providerPath.startsWith("jceks://file")) {
                keystore = providerPath.replace("jceks://file", "");
            } else if (providerPath.startsWith("localjceks://file")) {
                keystore = providerPath.replace("jceks://file", "");
            } else if (providerPath.startsWith("bcfks://file")) {
                keystore = providerPath.replace("bcfks://file", "");
            } else if (providerPath.startsWith("localbcfks://file")) {
                keystore = providerPath.replace("bcfks://file", "");
            } else {
                keystore = providerPath;
            }
            if (keystore != null && !keystore.isEmpty()) {
                File file = new File(keystore);
                if (file != null && file.exists() && file.length() == 0) {
                    System.out.println("Provider file '" + keystore + "' is in invalid state or corrupt!! will try to delete first.");
                    file.delete();
                    file = null;
                }
            }
        }
    }

    private static boolean isCredentialShellInteractiveEnabled() {
        boolean ret = false;

        String fieldName = "interactive";

        CredentialShell cs = new CredentialShell();

        try {
            Field interactiveField = cs.getClass().getDeclaredField(fieldName);

            if (interactiveField != null) {
                interactiveField.setAccessible(true);
                ret = interactiveField.getBoolean(cs);
                System.out.println("FOUND value of [" + fieldName + "] field in the Class [" + cs.getClass().getName() + "] = [" + ret + "]");
            }
        } catch (Throwable e) {
            System.out.println("Unable to find the value of [" + fieldName + "] field in the Class [" + cs.getClass().getName() + "]. Skiping -f option");
            e.printStackTrace();
            ret = false;
        }

        return ret;
    }
}
